package jinyilw.common.database;

import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.util.Arrays;

/**
 * This class can split SQL scripts to single SQL statements. Each SQL statement
 * ends with the character ';', however it is ignored in comments and quotes.
 */
public class ScriptReader implements Closeable
{

	/**
	 * The block size for I/O operations.
	 */
	public static final int IO_BUFFER_SIZE = 4 * 1024;

	private final Reader reader;
	private char[] buffer;

	/**
	 * The position in the buffer of the next char to be read
	 */
	private int bufferPos;

	/**
	 * The position in the buffer of the statement start
	 */
	private int bufferStart = -1;

	/**
	 * The position in the buffer of the last available char
	 */
	private int bufferEnd;

	/**
	 * True if we have read past the end of file
	 */
	private boolean endOfFile;

	/**
	 * True if we are inside a comment
	 */
	private boolean insideRemark;

	/**
	 * Only valid if insideRemark is true. True if we are inside a block
	 * comment, false if we are inside a line comment
	 */
	private boolean blockRemark;

	/**
	 * True if comments should be skipped completely by this reader.
	 */
	private boolean skipRemarks;

	/**
	 * The position in buffer of start of comment
	 */
	private int remarkStart;

	/**
	 * Create a new SQL script reader from the given reader
	 *
	 * @param reader the reader
	 */
	public ScriptReader(Reader reader)
	{
		this.reader = reader;
		buffer = new char[IO_BUFFER_SIZE * 2];
	}

	/**
	 * Close the underlying reader.
	 */
	@Override
	public void close()
	{
		try
		{
			reader.close();
		} catch (IOException e)
		{
			e.printStackTrace();
			// throw DbException.convertIOException(e, null);
		}
	}

	/**
	 * Read a statement from the reader. This method returns null if the end has
	 * been reached.
	 *
	 * @return the SQL statement or null
	 */
	public String readStatement()
	{
		if (!endOfFile)
			try
			{
				return readStatementLoop();
			} catch (IOException e)
			{
				e.printStackTrace();
			}
		return null;
	}

	private String readStatementLoop() throws IOException
	{
		bufferStart = bufferPos;
		int c = read();
		while (true)
		{
			if (c < 0)
			{
				endOfFile = true;
				if (bufferPos - 1 == bufferStart)
				{
					return null;
				}
				break;
			} else if (c == ';')
			{
				break;
			}
			switch (c)
			{
				case '$':
				{
					c = read();
					if (c == '$' && (bufferPos - bufferStart < 3
							|| buffer[bufferPos - 3] <= ' '))
					{
						while (true)
						{
							c = read();
							if (c < 0)
							{
								break;
							}
							if (c == '$')
							{
								c = read();
								if (c < 0)
								{
									break;
								}
								if (c == '$')
								{
									break;
								}
							}
						}
						c = read();
					}
					break;
				}
				case '\'':
					while (true)
					{
						c = read();
						if (c < 0)
						{
							break;
						}
						if (c == '\'')
						{
							break;
						}
					}
					c = read();
					break;
				case '"':
					while (true)
					{
						c = read();
						if (c < 0)
						{
							break;
						}
						if (c == '\"')
						{
							break;
						}
					}
					c = read();
					break;
				case '/':
				{
					c = read();
					if (c == '*')
					{
						startRemark(true);
						while (true)
						{
							c = read();
							if (c < 0)
							{
								break;
							}
							if (c == '*')
							{
								c = read();
								if (c < 0)
								{
									clearRemark();
									break;
								}
								if (c == '/')
								{
									endRemark();
									break;
								}
							}
						}
						c = read();
					} else if (c == '/')
					{
						startRemark(false);
						while (true)
						{
							c = read();
							if (c < 0)
							{
								clearRemark();
								break;
							}
							if (c == '\r' || c == '\n')
							{
								endRemark();
								break;
							}
						}
						c = read();
					}
					break;
				}
				case '-':
				{
					c = read();
					if (c == '-')
					{
						startRemark(false);
						while (true)
						{
							c = read();
							if (c < 0)
							{
								clearRemark();
								break;
							}
							if (c == '\r' || c == '\n')
							{
								endRemark();
								break;
							}
						}
						c = read();
					}
					break;
				}
				default:
				{
					c = read();
				}
			}
		}
		return new String(buffer, bufferStart, bufferPos - 1 - bufferStart);
	}

	private void startRemark(boolean block)
	{
		blockRemark = block;
		remarkStart = bufferPos - 2;
		insideRemark = true;
	}

	private void endRemark()
	{
		clearRemark();
		insideRemark = false;
	}

	private void clearRemark()
	{
		if (skipRemarks)
		{
			Arrays.fill(buffer, remarkStart, bufferPos, ' ');
		}
	}

	private int read() throws IOException
	{
		if (bufferPos >= bufferEnd)
		{
			return readBuffer();
		}
		return buffer[bufferPos++];
	}

	private int readBuffer() throws IOException
	{
		if (endOfFile)
		{
			return -1;
		}
		int keep = bufferPos - bufferStart;
		if (keep > 0)
		{
			char[] src = buffer;
			if (keep + IO_BUFFER_SIZE > src.length)
			{
				if (src.length >= Integer.MAX_VALUE / 2)
				{
					throw new IOException("Error in parsing script, "
							+ "statement size exceeds 1G, "
							+ "first 80 characters of statement looks like: "
							+ new String(buffer, bufferStart, 80));
				}
				buffer = new char[src.length * 2];
			}
			System.arraycopy(src, bufferStart, buffer, 0, keep);
		}
		remarkStart -= bufferStart;
		bufferStart = 0;
		bufferPos = keep;
		int len = reader.read(buffer, keep, IO_BUFFER_SIZE);
		if (len == -1)
		{
			// ensure bufferPos > bufferEnd
			bufferEnd = -1024;
			endOfFile = true;
			// ensure the right number of characters are read
			// in case the input buffer is still used
			bufferPos++;
			return -1;
		}
		bufferEnd = keep + len;
		return buffer[bufferPos++];
	}

	/**
	 * Check if this is the last statement, and if the single line or block
	 * comment is not finished yet.
	 *
	 * @return true if the current position is inside a remark
	 */
	public boolean isInsideRemark()
	{
		return insideRemark;
	}

	/**
	 * If currently inside a remark, this method tells if it is a block comment
	 * (true) or single line comment (false)
	 *
	 * @return true if inside a block comment
	 */
	public boolean isBlockRemark()
	{
		return blockRemark;
	}

	/**
	 * If comments should be skipped completely by this reader.
	 *
	 * @param skipRemarks true if comments should be skipped
	 */
	public void setSkipRemarks(boolean skipRemarks)
	{
		this.skipRemarks = skipRemarks;
	}
}
